package com.cloud.gateway.filter;

import com.alibaba.csp.sentinel.AsyncEntry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.cloud.common.bean.ResultInfo;
import com.cloud.common.bean.SessionInfo;
import com.cloud.common.config.WebConfig;
import com.cloud.common.context.AppContext;
import com.cloud.common.context.SessionContext;
import com.cloud.common.context.VersionContext;
import com.cloud.common.ribbon.RibbonBestRule;
import com.cloud.common.util.JsonUtils;
import com.cloud.gateway.bean.ReleaseInfo;
import com.cloud.gateway.cache.AccessCache;
import com.cloud.gateway.cache.LoginSessionCache;
import com.cloud.gateway.cache.ReleaseCache;
import com.cloud.gateway.config.GatewayConfig;
import com.cloud.gateway.config.MockConfig;
import com.cloud.gateway.support.WebSupport;
import com.netflix.loadbalancer.LoadBalancerStats;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;

@Slf4j
@Component
public class WebGatewayFilter implements GlobalFilter, Ordered {
    private static final String ALL = "*";
    private static final String WEBSOCKET = "websocket";

    @Autowired
    private MockConfig mockConfig;

    @Autowired
    private GatewayConfig gatewayConfig;

    @Autowired
    private LoginSessionCache sessionCache;

    @Autowired
    private ReleaseCache releaseCache;

    @Autowired
    private AccessCache accessCache;

    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String requestURI = request.getURI().getPath();

        // 支持mock
        if (mockConfig.isEnable() && !AppContext.isProdEnv()) {
            return doMock(requestURI, response);
        }

        // 系统升级维护
        if (gatewayConfig.isUpgrade()) {
            ResultInfo<String> resultInfo = ResultInfo.failure();
            resultInfo.setCode(HttpStatus.NOT_IMPLEMENTED.value());
            return responseResultInfo(response, resultInfo);
        }

        // 获取令牌
        String token = getToken(request);
        if (!AppContext.isProdEnv()) {
            log.info("requestURI={}, token={}", requestURI, token);
        }  else {
            log.debug("requestURI={}, token={}", requestURI, token);
        }

        // 判断是否登录地址
        if (isLoginURI(requestURI)) {
            SessionInfo sessionInfo = SessionInfo.buildDefault();
            return handleContext(chain, exchange, sessionInfo);
        }

        // 判断是否检查token
        if (StringUtils.isEmpty(token) && (isExcludeURI(requestURI) || isDebug(request))) {
            SessionInfo sessionInfo = SessionInfo.buildDefault();
            return handleContext(chain, exchange, sessionInfo);
        }

        return sessionCache.get(token).switchIfEmpty(responseAuthFailure(response)).flatMap(data -> {
            SessionInfo sessionInfo = (SessionInfo) data;
            // 检查授权资源
            if (!checkAuth(requestURI, sessionInfo)) {
                return responseResultInfo(response, ResultInfo.failure().setCode(HttpStatus.FORBIDDEN.value()));
            }

            // 检查白名单IP
            Set<String> whitelistIps = sessionInfo.getWhitelistIps();
            if (!whitelistIps.contains(ALL) && !whitelistIps.contains(WebSupport.getRequestIP(request))) {
                return responseResultInfo(response, ResultInfo.failure().setCode(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value()));
            }
            return handleContext(chain, exchange, sessionInfo);
        });
    }

    private boolean isDebug(ServerHttpRequest request) {
        if (AppContext.isProdEnv()) {
            return false;
        }
        // 非生产环境并且请求头有debug标识
        String debug = request.getHeaders().getFirst(SessionInfo.DEBUG);
        if (StringUtils.isNotEmpty(debug) || gatewayConfig.isDebug()) {
            return true;
        }
        return false;
    }

    private Mono<Void> doMock(String requestURI, ServerHttpResponse response) {
        List<String> requestUrls = mockConfig.getRequestUrls();
        for (String url : requestUrls) {
            String[] arr = url.split("@");
            if (requestURI.equals(arr[0])) {
                RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
                ResultInfo resultInfo = restTemplate.postForObject(arr[1], null, ResultInfo.class);
                return responseResultInfo(response, resultInfo);
            }
        }
        return responseResultInfo(response, ResultInfo.failure());
    }

    private Mono<Void> responseAuthFailure(ServerHttpResponse response) {
        return Mono.defer(() -> responseResultInfo(response, ResultInfo.failure().setCode(HttpStatus.UNAUTHORIZED.value())));
    }

    /**
     * 检查授权资源
     *
     * @param requestURI
     * @param sessionInfo
     * @return boolean
     */
    private boolean checkAuth(String requestURI, SessionInfo sessionInfo) {
        if (isExcludeURI(requestURI)) {
            return true;
        }
        for (String resource : sessionInfo.getAuthResources()) {
            if (requestURI.startsWith(resource)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 处理上下文
     *
     * @param chain
     * @param exchange
     * @param sessionInfo
     * @return
     */
    private Mono<Void> handleContext(GatewayFilterChain chain, ServerWebExchange exchange, SessionInfo sessionInfo) {
        String requestURI = exchange.getRequest().getURI().getPath();

        // 保存访问记录
        accessCache.save(requestURI, sessionInfo);

        // 获取版本设置到上下文
        String version = getReleaseVersion(sessionInfo);
        VersionContext.set(version);

        // 支持WebSocket
        if (requestURI.contains(WEBSOCKET)) {
            return chain.filter(exchange);
        }

        // 设置请求头
        ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
        requestBuilder.header(WebConfig.FROM_GATEWAY, Boolean.TRUE.toString());
        if (version != null) {
            requestBuilder.header(VersionContext.VERSION_INFO, StringUtils.stripAll(version));
        }
        if (sessionInfo != null) {
            SessionInfo transferSession = new SessionInfo();
            transferSession.setAppName(sessionInfo.getAppName());
            transferSession.setUserId(sessionInfo.getUserId());
            if (SessionInfo.DEBUG.equals(sessionInfo.getSecret())) {
                transferSession.setSecret(sessionInfo.getSecret());
            }
            requestBuilder.header(SessionContext.SESSION_INFO, StringUtils.stripAll(JsonUtils.toJSONString(transferSession)));
            if (sessionInfo.isTraced()) {
                log.info("userId={} requestURI={}", sessionInfo.getUserId(), exchange.getRequest().getURI().getPath());
            }
        }

        // 处理结束
        ServerWebExchange webExchange = exchange.mutate().request(requestBuilder.build()).build();
        return chainFilter(chain, webExchange);
    }

    /**
     * 执行过滤器链
     *
     * @param chain
     * @param exchange
     * @return Mono<Void>
     */
    private Mono<Void> chainFilter(GatewayFilterChain chain, ServerWebExchange exchange) {
        String requestURI = exchange.getRequest().getURI().getPath();
        try {
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            // Sentinel检查
            ContextUtil.enter(route.getUri().toString());
            AsyncEntry entry = SphU.asyncEntry(requestURI, EntryType.IN);

            long beginTime = System.currentTimeMillis();
            return chain.filter(exchange).doOnError(error -> {
                Tracer.traceEntry(error, entry);
                handleError(exchange, error);
            }).doFinally(type -> {
                logTime(requestURI, beginTime);
                VersionContext.remove();
                entry.exit();
            });
        } catch (BlockException ex) {
            log.warn("{} execute break", requestURI);
            return responseResultInfo(exchange.getResponse(), new ResultInfo().setCode(HttpStatus.LOCKED.value()));
        }
    }

    private void handleError(ServerWebExchange exchange, Throwable error) {
        boolean unavailable = false;
        if (error instanceof ResponseStatusException && ((ResponseStatusException) error).getStatus() == HttpStatus.GATEWAY_TIMEOUT) {
            unavailable = true;
        }
        if (error instanceof SocketException || error instanceof SocketTimeoutException) {
            unavailable = true;
        }
        if (unavailable) {
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            if (!route.getUri().getScheme().equals("lb")) {
                return;
            }
            URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
            Server server = new Server(url.getScheme(), url.getHost(), url.getPort());

            String serviceName =  route.getUri().getHost();
            LoadBalancerStats loadBalancerStats = RibbonBestRule.getInstance(serviceName).getLoadBalancerStats();
            loadBalancerStats.getSingleServerStat(server).incrementSuccessiveConnectionFailureCount();
        }
    }

    private void logTime(String requestURI, long beginTime) {
        long costTime = System.currentTimeMillis() - beginTime;
        if (costTime >= 3000) {
            log.warn("{} ExecuteBlocking={}", requestURI, costTime);
        } else if (costTime >= 500) {
            log.info("{} ExecuteSlowness={}", requestURI, costTime);
        }

        if (log.isDebugEnabled()) {
            log.debug("{} ExecuteTime={}", requestURI, costTime);
        }
    }

    /**
     * 输出响应信息
     *
     * @param response
     * @param resultInfo
     */
    private Mono<Void> responseResultInfo(ServerHttpResponse response, ResultInfo resultInfo) {
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        byte[] bytes = JsonUtils.toJSONString(resultInfo).getBytes(StandardCharsets.UTF_8);
        return response.writeWith(Flux.just(response.bufferFactory().wrap(bytes)));
    }

    /**
     * 是否登录地址
     *
     * @param requestURI
     * @return boolean
     */
    private boolean isLoginURI(String requestURI) {
        List<String> loginURISuffixs = gatewayConfig.getLoginURISuffixs();
        if (loginURISuffixs != null) {
            for (String uri : loginURISuffixs) {
                if (requestURI.endsWith(uri)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 是否排除
     *
     * @param requestURI
     * @return boolean
     */
    private boolean isExcludeURI(String requestURI) {
        List<String> excludeURIPrefixs = gatewayConfig.getExcludeURIPrefixs();
        if (excludeURIPrefixs != null) {
            for (String uri : excludeURIPrefixs) {
                if (requestURI.startsWith(uri)) {
                    return true;
                }
            }
        }
        List<String> excludeURISuffixs = gatewayConfig.getExcludeURISuffixs();
        if (excludeURISuffixs != null) {
            for (String uri : excludeURISuffixs) {
                if (requestURI.endsWith(uri)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 获取发布版本
     *
     * @param sessionInfo
     * @return String
     */
    private String getReleaseVersion(SessionInfo sessionInfo) {
        List<ReleaseInfo> list = releaseCache.getReleases();
        for (ReleaseInfo releaseInfo : list) {
            String userId = releaseInfo.getUserId();
            String userArea = releaseInfo.getUserArea();
            if (userId.equals(ALL) || userArea.equals(ALL)) {
                return releaseInfo.getVersion();
            }
            if (sessionInfo != null) {
                if (userId.contains(sessionInfo.getUserId())) {
                    return releaseInfo.getVersion();
                }
                if (StringUtils.isNotEmpty(sessionInfo.getAreaCode()) && userArea.contains(sessionInfo.getAreaCode())) {
                    return releaseInfo.getVersion();
                }
            }
        }
        return null;
    }

    /**
     * 获取令牌
     *
     * @param request
     * @return String
     */
    private String getToken(ServerHttpRequest request) {
        String token = request.getQueryParams().getFirst(SessionContext.TOKEN);
        token = token == null ? request.getHeaders().getFirst(SessionContext.TOKEN) : token;
        return token;
    }
}
